Skip to content

Derive the loopback listener inventory from the lint - #635

Merged
nedtwigg merged 8 commits into
mainfrom
fix/loopback-inventory-fourth
Sep 15, 2026
Merged

nedtwigg merged 8 commits into
mainfrom
fix/loopback-inventory-fourth

Conversation

@dormouse-bot

@dormouse-bot dormouse-bot commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

The security spec had a hand-maintained listener count and list. They drifted when scripts/direct-interop/run.mjs added another loopback listener.

This PR removes that duplicated inventory. The lint is now the live inventory:

  • It scans every tracked JavaScript and TypeScript file.
  • It prints every bind site it recognizes.
  • It separates non-test listeners, test listeners, and its own fixtures.
  • Non-test listeners still fail unless they reference a guard or have a documented allowlist entry.
  • The self-test now proves that test listeners are reported without being treated as product listeners.

The audit prompt tells the auditor to inspect every non-test result, skip the clearly labeled test and self-test results, and search the same files manually for new APIs and hosts built at runtime.

The spec no longer contains a count or path list that can drift. It keeps the rule and the lint's known ceiling. The Relay remains separate because no foreign browser origin may drive its API, whatever interface it binds; security-remote.md owns that rule.

Why not say all source code? The lint only scans tracked JavaScript and TypeScript. Saying all source code would also claim Rust, shell, generated files, dependencies, and server APIs the matcher does not know.

Current live inventory: four non-test bind sites, seven test bind sites in six files, and nine textual matches in the self-test fixture. These numbers are output, not copied into the spec.

Verification: pnpm test passes on the rebased branch.

…opback inventory

scripts/direct-interop/run.mjs binds a loopback HTTP server at :214 and has
since it landed, so loopback-lint reports four listeners while the spec's
"Loopback Listeners" section still says "Today the set is three". The harness
is guarded — it imports isOwnOrigin from lib/src/host/loopback-guard.ts and
isAuthorized from standalone/scripts/dev-host-guard.mjs — so the lint is green
and only the spec's own inventory is stale. Nothing else changes: the rule, the
FAIL IF, and the lint are all untouched.

Found by the 2026-09-13 nightly security audit (W1, refs #598).
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 13, 2026

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 973b70e
Status: ✅  Deploy successful!
Preview URL: https://0c8d2e16.mouseterm.pages.dev
Branch Preview URL: https://fix-loopback-inventory-fourt.mouseterm.pages.dev

View logs

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feedback on work in progress, not a merge verdict — mark the PR ready when you want the full review.

The correction itself checks out: scripts/direct-interop/run.mjs binds server.listen(0, '127.0.0.1', …), imports both isOwnOrigin and isAuthorized, and gates on them at the top of the handler, so it passes the lint by reference rather than by allowlist. loopback-lint reports 4 loopback listeners, 1 allowlisted and its self-test stays green at 7 load-bearing checks; the two spec-lint path misses are the standalone/sidecar/node_modules artifacts of an un-installed checkout, not this diff.

One thing keeps the fix from holding: the sentence's derivation instruction still says "shipped trees", which is a narrower scope than the four it now counts, and .github/audit/application-security.md repeats that instruction to the very domain that raised W1. Inline suggestion below.

One case the wider scope surfaces and this bullet doesn't address either way: relay/scripts/dev.mjs defaults DORMOUSE_BIND_HOST to 127.0.0.1 and calls server.listen(port, env.DORMOUSE_BIND_HOST), a loopback HTTP-and-WebSocket bind the regex can't see because the host is a runtime value — the case the lint's header names as its own ceiling. The front matter defers the network boundary to docs/specs/security-remote.md, so it may well be out of scope here, but nothing in the bullet says so, and an auditor deriving mechanically lands on it. Saying once why the relay is not in this set would stop the next derivation re-opening it.

Comment thread docs/specs/security-local.md Outdated
…e set it names

The bullet said to derive the set by searching the "shipped trees", but two of
the four listeners it now names — the browser-dev bridge and the direct-path
interop harness — are in neither shipped tree; loopback-lint reaches both only
because it scans git ls-files for tracked, non-test source files. An auditor
following the old instruction derives two and re-files the same miscount.
.github/audit/application-security.md repeated the instruction verbatim to the
domain that raised it, so both lines move together.

Also state once why the Relay is out of this set. It binds from config rather
than to render a surface, and the front matter already defers the network
boundary to docs/specs/security-remote.md — but nothing in the bullet said so,
and the wider derivation scope now lands a mechanical search on
relay/scripts/dev.mjs.

docs/specs/security-local.md: 2533 / 2550 words, no re-baseline.
@nedtwigg
nedtwigg marked this pull request as ready for review September 15, 2026 17:33

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The count, the derivation instruction, and the audit prompt all check out: pnpm lint:loopback reports 4 loopback listeners, 1 allowlisted, scripts/direct-interop/run.mjs gates on isAuthorized and isOwnOrigin before routing, and sourceFiles() in scripts/loopback-lint.mjs really does scan git ls-files for tracked, non-test source files — so .github/audit/application-security.md:45 now names the scope the lint scans.

Two things the correction leaves behind.

The Relay deferral doesn't close the question it was added to close. Inline suggestion on the bullet — the stated reason isn't true of the one Relay bind that is loopback, and the spec-level pointer lands the reader on the section about the installer rather than the one about what the listener admits.

scripts/loopback-lint.mjs's own header still narrates the pre-PR count. Its "Why this exists" paragraph says "Two of the three listeners we ship got that wrong at some point" and "so a fourth listener fails a build instead of waiting for the next audit to notice it" — written when the set was three, and now read by an editor at the moment they are adding a bind form. That is the same count-shaped claim nothing pins that this PR exists to correct, one file over, and "a fourth listener" now names one that already exists. I'm pushing a count-free rewording of those two clauses rather than incrementing them, so the next listener doesn't reopen this.

Comment thread docs/specs/security-local.md Outdated
…nswers it

The bullet said the Relay was out of scope because "it binds from config",
but relay/scripts/dev.mjs defaults DORMOUSE_BIND_HOST to 127.0.0.1 — the one
Relay bind a mechanical derivation lands on is loopback, so the stated reason
did not cover it. Name the dev runner, and point at security-remote.md ->
"Cross-origin access" (no CORS, no cookie auth) rather than the spec as a
whole, which sends the reader to "Network posture" and the installer's bind
pin instead. Word-neutral: 2649 / 2650 merged with main.

Drop the matching count from scripts/loopback-lint.mjs's header, which still
narrated three listeners and a hypothetical fourth. The inventory lives in
the spec; a count in two places drifts in one of them.
@dormouse-bot
dormouse-bot force-pushed the fix/loopback-inventory-fourth branch from ca39cc7 to e754820 Compare September 15, 2026 17:45

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The count, the derivation scope, and the audit prompt all hold: pnpm lint:loopback reports 4 loopback listeners, 1 allowlisted, scripts/direct-interop/run.mjs gates on isAuthorized and isOwnOrigin before routing, and loopback-lint-selftest stays green at 7 load-bearing checks. The lint-header rewording lands — no inventory count survives in that file.

The Relay exclusion is now false in the other direction. The bullet says "only relay/scripts/dev.mjs binds loopback", but relay/src/start.ts binds serve({ fetch, port, ...(bindHost ? { hostname: bindHost } : {}) }) with bindHost from DORMOUSE_BIND_HOST, and docs/specs/security-remote.md -> "Network posture (self-hosted)" pins that to 127.0.0.1 in all three installers — "The shipped self-host deployment is a per-login user agent bound to loopback". So the shipped Relay binds loopback, and an auditor who checks the pointer finds the exclusion contradicted by the spec it cites. scripts/loopback-lint.mjs's own header states the accurate version one file over: "relay/ does it deliberately from config". The reason that does hold is the one the pointer already names — no foreign browser origin may drive the API, whatever interface it binds. Inline suggestion below.

The fix needs budget headroom this branch doesn't have. The branch is behind main on this spec: docs/specs/security-local.md is 2533 words at e754820b but 2649 on the merge ref, against main's 2650 budget — one word. The suggestion is +4, and ratcheting on the branch as it stands would write 2550 into scripts/spec-word-budgets.json, which the merge would then prefer over main's 2650 and fail at 2653. Merging main in first, then ratcheting, is what keeps CI green.

One thing the diff leaves adjacent: .github/audit/application-security.md still opens that paragraph with "read lib/src/host/loopback-guard.ts first — it states the rule — then each listener it names", and the guard names no inventory on purpose — its header says the spec "is deliberately not restated here, since it tells its reader to derive that set by search rather than trust a list". An auditor reading "each listener it names" finds one mention of the iframe proxy and no set. Since the next sentence in the same paragraph is what this PR corrects, the antecedent belongs to the section, not the guard.

Pushing all of it — the merge, the reworded exclusion, the ratchet, and the antecedent — as one commit.

Comment thread docs/specs/security-local.md Outdated
dormouse-bot and others added 3 commits September 15, 2026 17:58
…re it binds

`relay/src/start.ts` binds `hostname: bindHost` from `DORMOUSE_BIND_HOST`, and
every installer pins that to 127.0.0.1 per `docs/specs/security-remote.md` ->
"Network posture (self-hosted)" — so "only relay/scripts/dev.mjs binds loopback"
was falsified by the very section the bullet points at. Rest the exclusion on
what the listener admits instead: no foreign browser origin may drive the API,
whatever interface it binds. That survives any bind host and matches the
"Cross-origin access" heading the pointer already names.

Also fix the antecedent in the audit prompt: `loopback-guard.ts` deliberately
names no inventory, so "each listener it names" sends the auditor to a file
that has none. The section is what names them.

Merges main first so the ratchet is computed against the size CI sees: the
spec lands at 2657, so the budget goes to 2700. (The review's "+4 words →
2653" predated the ", whatever interface it binds" clause; the ratchet is
needed either way.)
@nedtwigg nedtwigg changed the title Count the direct-path interop harness in security-local.md's loopback inventory Derive the loopback listener inventory from the lint Sep 15, 2026

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lint change holds where it counts: node scripts/loopback-lint.mjs on the
merge ref prints four non-test listeners, seven test listeners and nine
self-test fixtures, and loopback-lint-selftest stays green at 8 load-bearing
checks — so the new test-classification branch is pinned the same way the seven
bind forms are. spec-lint is clean apart from the two
standalone/sidecar/node_modules path misses an un-installed checkout always
reports.

The word budget is 50 words looser than the file earns. countWords puts
docs/specs/security-local.md at 2605 on the merge ref, which rounds to 2650 —
the value main already carries. The 2657 → 2700 arithmetic in 1ec1323b's
message was true of that commit, but da7e6a0f rewrote the bullet shorter and
the ratchet was not re-run. AGENTS.md pins a budget to "its size rounded up to
the nearest 50", and check 10 only fails upward, so a stale-high budget rides
green through CI while handing the next editor 95 unearned words — two rules at
the 46-word corpus median the step size exists to make expensive. The whole
scripts/spec-word-budgets.json hunk can come out of the PR.

Three smaller ones inline: the self-test's failure hint quotes a spec clause
this PR deleted, the audit prompt routes the auditor through the mutating
self-test, and withAppendedOutput re-implements two things lint-kit already
has.

Comment thread scripts/spec-word-budgets.json Outdated
Comment thread scripts/loopback-lint-selftest.mjs Outdated
Comment thread .github/audit/application-security.md Outdated
Comment thread scripts/lint-kit.mjs

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inventory move holds where it counts: node scripts/loopback-lint.mjs prints
four non-test listeners, seven test listeners and nine self-test fixtures,
loopback-lint-selftest stays green at 8 load-bearing checks, and
scripts/spec-word-budgets.json is out of the diff — docs/specs/security-local.md
is inside main's 2650 without a ratchet. The self-test hint now quotes a clause
that exists (A new non-test listener without a guard reference fails the build,
docs/specs/security-local.md line 119), and node scripts/loopback-lint.mjs in
the audit prompt gets the auditor the inventory without the self-test's eight file
mutations.

One gap the new wording opens.

The lint's file filter is narrower than the scope three files now claim for it.
SOURCE_EXT in scripts/loopback-lint.mjs is /\.(?:ts|tsx|js|jsx|mjs|cjs)$/
every JavaScript variant, but only two of TypeScript's four. The spec bullet now
says the lint "scans all tracked JavaScript and TypeScript and prints every bind it
recognizes", .github/audit/application-security.md says "The lint scans all
tracked JavaScript and TypeScript" and then sends the auditor to "the same files"
for the manual sweep, and the lint's own header says "scans every tracked
JavaScript and TypeScript file". A loopback bind in a .mts or .cts file is
caught by none of the three: pnpm test stays green with no guard reference, the
printed inventory omits it, and the nightly auditor reading "the same files"
excludes it too. vscode-ext/vitest.config.mts and
vscode-ext/vitest.smoketest.config.mts are tracked today, so the extension is in
use — it is the count-shaped drift this PR removes from the spec, relocated one
extension over.

Neither .mts file binds anything, so widening the filter changes today's output
not at all. Inline suggestion below; pushing it as a commit, since it is the fix
this review already worked out and the PR has no human author to apply it.

Comment thread scripts/loopback-lint.mjs Outdated
The spec bullet, the application-security prompt and this lint's own header
all say it scans every tracked JavaScript and TypeScript file, but SOURCE_EXT
listed every JavaScript variant and only two of TypeScript's four. Two tracked
.mts files exist, so a loopback bind in one was invisible to the deterministic
check and excluded from the manual sweep the prompt sends the auditor on.

The self-test gains a case that appends a bind to a tracked .mts file and
requires the lint to go red, so the extension scope is pinned the same way the
seven bind forms and the test classification are. Verified load-bearing:
narrowing SOURCE_EXT back makes the new case the only weak one.
@nedtwigg
nedtwigg merged commit 23d67fa into main Sep 15, 2026
10 checks passed
@nedtwigg
nedtwigg deleted the fix/loopback-inventory-fourth branch September 15, 2026 19:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants